using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.Text;

//
// * Copyright (c) 2002-2009 "Neo Technology,"
// *     Network Engine for Objects in Lund AB [http://neotechnology.com]
// *
// * This file is part of Neo4j.
// * 
// * Neo4j is free software: you can redistribute it and/or modify
// * it under the terms of the GNU Affero General Public License as
// * published by the Free Software Foundation, either version 3 of the
// * License, or (at your option) any later version.
// * 
// * This program is distributed in the hope that it will be useful,
// * but WITHOUT ANY WARRANTY; without even the implied warranty of
// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// * GNU Affero General Public License for more details.
// * 
// * You should have received a copy of the GNU Affero General Public License
// * along with this program. If not, see <http://www.gnu.org/licenses/>.
// 
namespace org.neo4j.kernel.impl.nioneo.store
{


///
/// <summary> * Implementation of the property store. This implementation has two dynamic
/// * stores. One used to store keys and another for string property values. </summary>
/// 
	public class PropertyStore : AbstractStore, Store
	{
	// store version, each store ends with this string (byte encoded)
		private const string VERSION = "PropertyStore v0.9.5";

	// record header size
	// in_use(byte)+type(int)+key_indexId(int)+prop_blockId(long)+
	// prev_prop_id(int)+next_prop_id(int)
		private const int RECORD_SIZE = 25;

		private const int STRING_STORE_BLOCK_SIZE = 120;

		private DynamicStringStore stringPropertyStore;
		private PropertyIndexStore propertyIndexStore;
		private DynamicArrayStore arrayPropertyStore;

///    
///     <summary> * See <seealso cref="AbstractStore#AbstractStore(String, Map)"/> </summary>
///     
		public PropertyStore<T1>(string fileName, Map<T1> config) : base(fileName, config)
		{
		}

///    
///     <summary> * See <seealso cref="AbstractStore#AbstractStore(String)"/> </summary>
///     
		public PropertyStore(string fileName) : base(fileName)
		{
		}

		protected internal override void initStorage()
		{
			stringPropertyStore = new DynamicStringStore(getStorageFileName() + ".strings", getConfig());
			propertyIndexStore = new PropertyIndexStore(getStorageFileName() + ".index", getConfig());
			File arrayStoreFile = new File(getStorageFileName() + ".arrays");
		// old store, create array store
			if (!arrayStoreFile.exists())
			{
				DynamicArrayStore.createStore(getStorageFileName() + ".arrays", STRING_STORE_BLOCK_SIZE);
			}
			arrayPropertyStore = new DynamicArrayStore(getStorageFileName() + ".arrays", getConfig());
		}

		protected internal override void setRecovered()
		{
			base.setRecovered();
			stringPropertyStore.setRecovered();
			propertyIndexStore.setRecovered();
			arrayPropertyStore.setRecovered();
		}

		protected internal override void unsetRecovered()
		{
			base.unsetRecovered();
			stringPropertyStore.unsetRecovered();
			propertyIndexStore.unsetRecovered();
			arrayPropertyStore.unsetRecovered();
		}

		protected internal override void closeStorage()
		{
			stringPropertyStore.Close();
			stringPropertyStore = null;
			propertyIndexStore.Close();
			propertyIndexStore = null;
			arrayPropertyStore.Close();
			arrayPropertyStore = null;
		}

		public override void flushAll()
		{
			stringPropertyStore.flushAll();
			propertyIndexStore.flushAll();
			arrayPropertyStore.flushAll();
			base.flushAll();
		}

		public virtual string getTypeAndVersionDescriptor()
		{
			return VERSION;
		}

		public override int getRecordSize()
		{
			return RECORD_SIZE;
		}

///    
///     <summary> * Creates a new property store contained in <CODE>fileName</CODE> If
///     * filename is <CODE>null</CODE> or the file already exists an 
///     * <CODE>IOException</CODE> is thrown.
///     *  </summary>
///     * <param name="fileName">
///     *            File name of the new property store </param>
///     * <exception cref="IOException">
///     *             If unable to create property store or name null </exception>
///     
		public static void createStore(string fileName)
		{
			createEmptyStore(fileName, VERSION);
			DynamicStringStore.createStore(fileName + ".strings", STRING_STORE_BLOCK_SIZE);
			PropertyIndexStore.createStore(fileName + ".index");
			DynamicArrayStore.createStore(fileName + ".arrays", STRING_STORE_BLOCK_SIZE);
		}

		private int nextStringBlockId()
		{
			return stringPropertyStore.nextBlockId();
		}

		public virtual void freeStringBlockId(int blockId)
		{
			stringPropertyStore.freeBlockId(blockId);
		}

		private int nextArrayBlockId()
		{
			return arrayPropertyStore.nextBlockId();
		}

		public virtual void freeArrayBlockId(int blockId)
		{
			arrayPropertyStore.freeBlockId(blockId);
		}

		public virtual PropertyIndexStore getIndexStore()
		{
			return propertyIndexStore;
		}

		public virtual void updateRecord(PropertyRecord record, bool recovered)
		{
			Debug.Assert(recovered);
			setRecovered();
			try
			{
				updateRecord(record);
			}
			finally
			{
				unsetRecovered();
			}
		}

		public virtual void updateRecord(PropertyRecord record)
		{
			PersistenceWindow window = acquireWindow(record.getId(), OperationType.WRITE);
			try
			{
				updateRecord(record, window);
			}
			finally
			{
				releaseWindow(window);
			}
			if (!record.isLight())
			{
				foreach (DynamicRecord valueRecord in record.getValueRecords())
				{
					if (valueRecord.getType() == PropertyType.STRING.intValue())
					{
						stringPropertyStore.updateRecord(valueRecord);
					}
					else if (valueRecord.getType() == PropertyType.ARRAY.intValue())
					{
						arrayPropertyStore.updateRecord(valueRecord);
					}
					else
					{
						throw new InvalidRecordException("Unknown dynamic record");
					}
				}
			}
		}

	// in_use(byte)+type(int)+key_blockId(int)+prop_blockId(long)+
	// prev_prop_id(int)+next_prop_id(int)

		private void updateRecord(PropertyRecord record, PersistenceWindow window)
		{
			int id = record.getId();
			Buffer buffer = window.getOffsettedBuffer(id);
			if (record.inUse())
			{
				buffer.put(Record.IN_USE.byteValue()).putInt(record.getType().intValue()).putInt(record.getKeyIndexId()).putLong(record.getPropBlock()).putInt(record.getPrevProp()).putInt(record.getNextProp());
			}
			else
			{
				buffer.put(Record.NOT_IN_USE.byteValue());
				if (!isInRecoveryMode())
				{
					freeId(id);
				}
			}
		}

		public virtual PropertyRecord getLightRecord(int id)
		{
			PersistenceWindow window = acquireWindow(id, OperationType.READ);
			try
			{
				PropertyRecord record = getRecord(id, window);
				record.setIsLight(true);
				return record;
			}
			finally
			{
				releaseWindow(window);
			}
		}

		public virtual void makeHeavy(PropertyRecord record)
		{
			record.setIsLight(false);
			if (record.getType() == PropertyType.STRING)
			{
				Collection<DynamicRecord> stringRecords = stringPropertyStore.getLightRecords((int) record.getPropBlock());
				foreach (DynamicRecord stringRecord in stringRecords)
				{
					stringRecord.setType(PropertyType.STRING.intValue());
					record.addValueRecord(stringRecord);
				}
			}
			else if (record.getType() == PropertyType.ARRAY)
			{
				Collection<DynamicRecord> arrayRecords = arrayPropertyStore.getLightRecords((int) record.getPropBlock());
				foreach (DynamicRecord arrayRecord in arrayRecords)
				{
					arrayRecord.setType(PropertyType.ARRAY.intValue());
					record.addValueRecord(arrayRecord);
				}
			}
		}

		public virtual PropertyRecord getRecord(int id)
		{
			PropertyRecord record;
			PersistenceWindow window = acquireWindow(id, OperationType.READ);
			try
			{
				record = getRecord(id, window);
			}
			finally
			{
				releaseWindow(window);
			}
			if (record.getType() == PropertyType.STRING)
			{
				Collection<DynamicRecord> stringRecords = stringPropertyStore.getLightRecords((int) record.getPropBlock());
				record.setIsLight(false);
				foreach (DynamicRecord stringRecord in stringRecords)
				{
					stringRecord.setType(PropertyType.STRING.intValue());
					record.addValueRecord(stringRecord);
				}
			}
			else if (record.getType() == PropertyType.ARRAY)
			{
				Collection<DynamicRecord> arrayRecords = arrayPropertyStore.getLightRecords((int) record.getPropBlock());
				record.setIsLight(false);
				foreach (DynamicRecord arrayRecord in arrayRecords)
				{
					arrayRecord.setType(PropertyType.ARRAY.intValue());
					record.addValueRecord(arrayRecord);
				}
			}
			return record;
		}

		private PropertyRecord getRecord(int id, PersistenceWindow window)
		{
			Buffer buffer = window.getOffsettedBuffer(id);
			if (buffer.get() != Record.IN_USE.byteValue())
			{
				throw new InvalidRecordException("Record[" + id + "] not in use");
			}
			PropertyRecord record = new PropertyRecord(id);
			record.setType(getEnumType(buffer.getInt()));
			record.setInUse(true);
			record.setKeyIndexId(buffer.getInt());
			record.setPropBlock(buffer.getLong());
			record.setPrevProp(buffer.getInt());
			record.setNextProp(buffer.getInt());
			return record;
		}

		private PropertyType getEnumType(int type)
		{
			switch (type)
			{
				case 1:
					return PropertyType.INT;
				case 2:
					return PropertyType.STRING;
				case 3:
					return PropertyType.BOOL;
				case 4:
					return PropertyType.DOUBLE;
				case 5:
					return PropertyType.FLOAT;
				case 6:
					return PropertyType.LONG;
				case 7:
					return PropertyType.BYTE;
				case 8:
					return PropertyType.CHAR;
				case 9:
					return PropertyType.ARRAY;
				case 10:
					return PropertyType.SHORT;
				default:
					throw new InvalidRecordException("Unknown enum type:" + type);
			}
		}

		public virtual object getValue(PropertyRecord propertyRecord)
		{
			PropertyType type = propertyRecord.getType();
			if (type == PropertyType.INT)
			{
				return (int) propertyRecord.getPropBlock();
			}
			if (type == PropertyType.STRING)
			{
				return getStringFor(propertyRecord);
			}
			if (type == PropertyType.BOOL)
			{
				if (propertyRecord.getPropBlock() == 1)
				{
					return Convert.ToBoolean(true);
				}
				return Convert.ToBoolean(false);
			}
			if (type == PropertyType.DOUBLE)
			{
				return new double(double.longBitsToDouble(propertyRecord.getPropBlock()));
			}
			if (type == PropertyType.FLOAT)
			{
				return new float(float.intBitsToFloat((int) propertyRecord.getPropBlock()));
			}
			if (type == PropertyType.LONG)
			{
				return propertyRecord.getPropBlock();
			}
			if (type == PropertyType.BYTE)
			{
				return (sbyte) propertyRecord.getPropBlock();
			}
			if (type == PropertyType.CHAR)
			{
				return (char) propertyRecord.getPropBlock();
			}
			if (type == PropertyType.ARRAY)
			{
				return getArrayFor(propertyRecord);
			}
			if (type == PropertyType.SHORT)
			{
				return (short) propertyRecord.getPropBlock();
			}
			throw new InvalidRecordException("Unknown type: " + type + " for record " + propertyRecord);
		}

		public override void makeStoreOk()
		{
			propertyIndexStore.makeStoreOk();
			stringPropertyStore.makeStoreOk();
			arrayPropertyStore.makeStoreOk();
			base.makeStoreOk();
		}

		public override void rebuildIdGenerators()
		{
			propertyIndexStore.rebuildIdGenerators();
			stringPropertyStore.rebuildIdGenerators();
			arrayPropertyStore.rebuildIdGenerators();
			base.rebuildIdGenerators();
		}

		public virtual void updateIdGenerators()
		{
			propertyIndexStore.updateIdGenerators();
			stringPropertyStore.updateHighId();
			arrayPropertyStore.updateHighId();
			this.updateHighId();
		}

		private Collection<DynamicRecord> allocateStringRecords(int valueBlockId, char[] chars)
		{
			return stringPropertyStore.allocateRecords(valueBlockId, chars);
		}

		private Collection<DynamicRecord> allocateArrayRecords(int valueBlockId, object array)
		{
			return arrayPropertyStore.allocateRecords(valueBlockId, array);
		}

		public virtual void encodeValue(PropertyRecord record, object @value)
		{
			if (@value is string)
			{
				int stringBlockId = nextStringBlockId();
				record.setPropBlock(stringBlockId);
				string @string = (string) @value;
				int length = @string.Length;
				char[] chars = new char[length];
				@string.getChars(0, length, chars, 0);
				Collection<DynamicRecord> valueRecords = allocateStringRecords(stringBlockId, chars);
				foreach (DynamicRecord valueRecord in valueRecords)
				{
					valueRecord.setType(PropertyType.STRING.intValue());
					record.addValueRecord(valueRecord);
				}
				record.setType(PropertyType.STRING);
			}
			else if (@value is int)
			{
				record.setPropBlock(((int) @value).intValue());
				record.setType(PropertyType.INT);
			}
			else if (@value is bool)
			{
				record.setPropBlock((((bool) @value).booleanValue() ? 1 : 0));
				record.setType(PropertyType.BOOL);
			}
			else if (@value is float)
			{
				record.setPropBlock(float.floatToRawIntBits(((float) @value).floatValue()));
				record.setType(PropertyType.FLOAT);
			}
			else if (@value is long)
			{
				record.setPropBlock(((long) @value).longValue());
				record.setType(PropertyType.LONG);
			}
			else if (@value is double)
			{
				record.setPropBlock(double.doubleToRawLongBits(((double) @value).doubleValue()));
				record.setType(PropertyType.DOUBLE);
			}
			else if (@value is sbyte)
			{
				record.setPropBlock(((sbyte) @value).byteValue());
				record.setType(PropertyType.BYTE);
			}
			else if (@value is char)
			{
				record.setPropBlock(((char) @value).charValue());
				record.setType(PropertyType.CHAR);
			}
			else if (@value.GetType().isArray())
			{
				int arrayBlockId = nextArrayBlockId();
				record.setPropBlock(arrayBlockId);
				Collection<DynamicRecord> arrayRecords = allocateArrayRecords(arrayBlockId, @value);
				foreach (DynamicRecord valueRecord in arrayRecords)
				{
					valueRecord.setType(PropertyType.ARRAY.intValue());
					record.addValueRecord(valueRecord);
				}
				record.setType(PropertyType.ARRAY);
			}
			else if (@value is short)
			{
				record.setPropBlock(((short) @value).shortValue());
				record.setType(PropertyType.SHORT);
			}
			else
			{
				throw new IllegalArgumentException("Unknown property type on: " + @value);
			}
		}

		public virtual object getStringFor(PropertyRecord propRecord)
		{
			int recordToFind = (int) propRecord.getPropBlock();
			IEnumerator <DynamicRecord> records = propRecord.getValueRecords().GetEnumerator();
			List<char[]> charList = new LinkedList<char[]>();
			int totalSize = 0;
			while (recordToFind != Record.NO_NEXT_BLOCK.intValue() && records.MoveNext())
			{
				DynamicRecord record = records.Current;
				if (record.getId() == recordToFind)
				{
					if (record.isLight())
					{
						stringPropertyStore.makeHeavy(record);
					}
					if (!record.isCharData())
					{
						ByteBuffer buf = ByteBuffer.wrap(record.getData());
						char[] chars = new char[record.getData().length / 2];
						totalSize += chars.Length;
						buf.asCharBuffer().get(chars);
						charList.Add(chars);
					}
					else
					{
						charList.Add(record.getDataAsChar());
					}
					recordToFind = record.getNextBlock();
				// TODO: make opti here, high chance next is right one
					records = propRecord.getValueRecords().GetEnumerator();
				}
			}
			StringBuilder buf = new StringBuilder();
			foreach (char[] str in charList)
			{
				buf.Append(str);
			}
			return buf.ToString();
		}

		public virtual object getArrayFor(PropertyRecord propertyRecord)
		{
			int recordToFind = (int) propertyRecord.getPropBlock();
			IEnumerator <DynamicRecord> records = propertyRecord.getValueRecords().GetEnumerator();
			List<sbyte[]> byteList = new LinkedList<sbyte[]>();
			int totalSize = 0;
			while (recordToFind != Record.NO_NEXT_BLOCK.intValue() && records.MoveNext())
			{
				DynamicRecord record = records.Current;
				if (record.getId() == recordToFind)
				{
					if (record.isLight())
					{
						arrayPropertyStore.makeHeavy(record);
					}
					if (!record.isCharData())
					{
						ByteBuffer buf = ByteBuffer.wrap(record.getData());
						sbyte[] bytes = new sbyte[record.getData().length];
						totalSize += bytes.Length;
						buf.get(bytes);
						byteList.Add(bytes);
					}
					else
					{
						throw new InvalidRecordException("Expected byte data on record " + record);
					}
					recordToFind = record.getNextBlock();
				// TODO: make opti here, high chance next is right one
					records = propertyRecord.getValueRecords().GetEnumerator();
				}
			}
			sbyte[] bArray = new sbyte[totalSize];
			int offset = 0;
			foreach (sbyte[] currentArray in byteList)
			{
				System.Array.Copy(currentArray, 0, bArray, offset, currentArray.length);
				offset += currentArray.length;
			}
			return arrayPropertyStore.getRightArray(bArray);
		}

		protected internal override bool versionFound(string version)
		{
			if (!version.StartsWith("PropertyStore"))
			{
			// non clean shutdown, need to do recover with right neo
				return false;
			}
			if (version.Equals("PropertyStore v0.9.3"))
			{
				rebuildIdGenerator();
				closeIdGenerator();
				return true;
			}
			throw new IllegalStoreVersionException("Store version [" + version + "]. Please make sure you are not running old Neo4j kernel " + " towards a store that has been created by newer version " + " of Neo4j.");
		}
	}
}